package com.varian.redis.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.varian.redis.annotation.VarianCache;
import com.varian.redis.annotation.VarianCacheEvict;
import com.varian.redis.annotation.VarianCachePut;
import com.varian.redis.exception.VariableNotAvailableException;
import com.varian.redis.operation.AbstractVarianOperation;
import com.varian.redis.operation.VarianCacheEvictOperation;
import com.varian.redis.operation.VarianCacheOperation;
import com.varian.redis.operation.VarianCachePutOperation;
import com.varian.redis.util.RedisUtil;
import com.varian.tool.Func;
import jodd.util.StringPool;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import static com.varian.redis.aspect.VarianCacheEvaluationContextFactory.NO_RESULT;
import static com.varian.redis.aspect.VarianCacheEvaluationContextFactory.RESULT_UNAVAILABLE;

/**
 * @author ben
 * @since 2024/9/29
 */
@Slf4j
public class VarianCacheAspectSupper {

    private final SpelExpressionParser parser = new SpelExpressionParser();
    private final VarianCacheEvaluationContextFactory evaluationContextFactory = new VarianCacheEvaluationContextFactory();
    private final Map<MethodClassKey, List<AbstractVarianOperation>> operationCache = new ConcurrentHashMap<>(1024);
    private final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS = Set.of(VarianCache.class, VarianCachePut.class, VarianCacheEvict.class);

    @SneakyThrows
    protected Object execute(VarianCacheInterceptor cacheInterceptor, Object target, Method method, Object[] args) {
        List<AbstractVarianOperation> operations = getOperations(target.getClass(), method);
        if (CollUtil.isNotEmpty(operations)) {
            VarianCacheContext cacheContext = new VarianCacheContext(operations, target, method, args);
            return execute(cacheContext, cacheInterceptor);
        }
        return cacheInterceptor.intercept();
    }

    @SneakyThrows
    private Object execute(VarianCacheContext cacheContext, VarianCacheInterceptor cacheInterceptor) {
        // Process any early evictions
        processCacheEvicts(cacheContext, cacheContext.getOperations(VarianCacheEvictOperation.class), true, NO_RESULT);
        // Check if we have a cached value matching the conditions
        Object cacheHit = findCachedValue(cacheContext);
        if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) {
            return evaluate(cacheContext, cacheInterceptor, cacheHit);
        }
        return cacheHit;
    }

    @SneakyThrows
    private Object evaluate(VarianCacheContext cacheContext, VarianCacheInterceptor cacheInterceptor, Object cacheHit) {
        // Re-invocation in reactive pipeline after late cache hit determination?
        if (cacheContext.isProcessed()) {
            return cacheHit;
        }

        Object cacheValue;
        Object returnValue;

        if (cacheHit != null && !hasCachePut(cacheContext)) {
            // If there are no put requests, just use the cache hit
            cacheValue = unwrapCacheValue(cacheHit);
            returnValue = cacheValue;
        } else {
            // Invoke the method if we don't have a cache hit
            returnValue = cacheInterceptor.intercept();
            cacheValue = unwrapReturnValue(returnValue);
        }

        // Collect puts from any @Cacheable miss, if no cached value is found
        List<AbstractVarianOperation> cachePutOperations = new ArrayList<>(1);
        EvaluationContext evaluationContext = createEvaluationContext(cacheContext, cacheValue);
        if (cacheHit == null) {
            collectPutKeys(cacheContext.getOperations(VarianCacheOperation.class), evaluationContext, cachePutOperations);
        }

        // Collect any explicit @CachePuts
        collectPutKeys(cacheContext.getOperations(VarianCachePutOperation.class), evaluationContext, cachePutOperations);

        // Process any collected put requests, either from @CachePut or a @Cacheable miss
        CollUtil.forEach(cachePutOperations, (operation, index) -> {
            String unless = "";
            long timeout = 0;
            TimeUnit unit = TimeUnit.SECONDS;
            if (operation instanceof VarianCacheOperation cacheOperation) {
                unless = cacheOperation.getUnless();
                timeout = cacheOperation.getTimeout();
                unit = cacheOperation.getUnit();
            } else if (operation instanceof VarianCachePutOperation cachePutOperation) {
                unless = cachePutOperation.getUnless();
                timeout = cachePutOperation.getTimeout();
                unit = cachePutOperation.getUnit();
            }
            if (StrUtil.isBlank(unless) || !unless(evaluationContext, unless)) {
                doPut(operation.getCacheName(), key(evaluationContext, operation.getKey()), cacheValue, timeout, unit);
            }
        });
        // Process any late evictions
        processCacheEvicts(cacheContext, cacheContext.getOperations(VarianCacheEvictOperation.class), false, returnValue);
        // Mark as processed for re-invocation after late cache hit determination
        cacheContext.setProcessed(true);

        return returnValue;
    }

    private void collectPutKeys(List<? extends AbstractVarianOperation> operations, EvaluationContext evaluationContext, List<AbstractVarianOperation> cachePutOperations) {
        CollUtil.forEach(operations, (operation, index) -> {
            if (isConditionPassing(evaluationContext, operation.getCondition())) {
                cachePutOperations.add(operation);
            }
        });
    }


    @Nullable
    private Object unwrapCacheValue(@Nullable Object cacheValue) {
        return (cacheValue instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheValue);
    }

    @Nullable
    private Object unwrapReturnValue(@Nullable Object returnValue) {
        return ObjectUtils.unwrapOptional(returnValue);
    }

    private boolean hasCachePut(VarianCacheContext cacheContext) {
        EvaluationContext evaluationContext = createEvaluationContext(cacheContext, RESULT_UNAVAILABLE);
        List<VarianCachePutOperation> cachePutOperations = cacheContext.getOperations(VarianCachePutOperation.class);
        List<VarianCachePutOperation> excluded = new ArrayList<>(1);
        CollUtil.forEach(cachePutOperations, (cachePutOperation, index) -> {
            try {
                if (!isConditionPassing(evaluationContext, cachePutOperation.getCondition())) {
                    excluded.add(cachePutOperation);
                }
            } catch (VariableNotAvailableException e) {
                // Ignoring failure due to missing result, consider the cache put has to proceed
            }
        });
        // Check if all puts have been excluded by condition
        return (cachePutOperations.size() != excluded.size());
    }

    private Object findCachedValue(VarianCacheContext cacheContext) {
        List<VarianCacheOperation> operations = cacheContext.getOperations(VarianCacheOperation.class);
        if (CollUtil.isNotEmpty(operations)) {
            for (VarianCacheOperation operation : operations) {
                EvaluationContext evaluationContext = createEvaluationContext(cacheContext, NO_RESULT);
                if (isConditionPassing(evaluationContext, operation.getCondition())) {
                    String cacheKey = key(evaluationContext, operation.getKey());
                    Object cached = doGet(operation.getCacheName(), cacheKey);
                    if (cached != null) {
                        Func.traceEnabled(log, logger -> logger.trace("Cache entry for key '{}' found in redis", cacheKey));
                        return cached;
                    } else {
                        Func.traceEnabled(log, logger -> logger.trace("No cache entry for key '{}' found in redis", cacheKey));
                    }
                }
            }
        }
        return null;
    }

    private void processCacheEvicts(VarianCacheContext cacheContext, List<VarianCacheEvictOperation> operations, boolean beforeInvocation, Object result) {
        if (CollUtil.isNotEmpty(operations)) {
            List<VarianCacheEvictOperation> evictOperations = operations.stream().filter(operation -> beforeInvocation == operation.isBeforeInvocation()).toList();
            performCacheEvicts(cacheContext, evictOperations, result);
        }
    }

    private void performCacheEvicts(VarianCacheContext cacheContext, List<VarianCacheEvictOperation> evictOperations, Object result) {
        CollUtil.forEach(evictOperations, (operation, index) -> {
            EvaluationContext evaluationContext = createEvaluationContext(cacheContext, result);
            if (isConditionPassing(evaluationContext, operation.getCondition())) {
                String cacheKey;
                if (operation.isAllEntries()) {
                    cacheKey = StringPool.STAR;
                } else {
                    cacheKey = key(evaluationContext, operation.getKey());
                }
                doDel(operation.getCacheName(), cacheKey);
            }
        });
    }

    private void doDel(String cacheName, String key) {
        RedisUtil.del(generateCacheKey(cacheName, key));
    }

    private void doPut(String cacheName, String key, Object value, long timeout, TimeUnit unit) {
        RedisUtil.Value.set(generateCacheKey(cacheName, key), value, timeout, unit);
    }

    private Object doGet(String cacheName, String key) {
        return RedisUtil.Value.get(generateCacheKey(cacheName, key));
    }

    private String generateCacheKey(String cacheName, String key) {
        return cacheName + StringPool.COLON + key;
    }


    private List<AbstractVarianOperation> getOperations(Class<?> clazz, Method method) {
        MethodClassKey methodClassKey = generateMethodClassKey(clazz, method);
        List<AbstractVarianOperation> operations;
        if (operationCache.containsKey(methodClassKey)) {
            operations = operationCache.get(methodClassKey);
        } else {
            operations = getAllCacheOperation(method);
            operationCache.put(methodClassKey, operations);
        }
        return operations;
    }

    private List<AbstractVarianOperation> getAllCacheOperation(Method method) {
        Set<Annotation> mergedAnnotations = AnnotatedElementUtils.getAllMergedAnnotations(method, CACHE_OPERATION_ANNOTATIONS);
        return mergedAnnotations.stream().map(annotation -> {
            if (annotation instanceof VarianCache varianCache) {
                return parseCacheAnnotation(varianCache);
            } else if (annotation instanceof VarianCachePut varianCachePut) {
                return parseCachePutAnnotation(varianCachePut);
            } else if (annotation instanceof VarianCacheEvict varianCacheEvict) {
                return parseCacheEvictAnnotation(varianCacheEvict);
            }
            return null;
        }).toList();
    }

    private boolean isConditionPassing(EvaluationContext evaluationContext, String conditionExpression) {
        return StrUtil.isBlank(conditionExpression) || Boolean.TRUE.equals(parser.parseExpression(conditionExpression).getValue(evaluationContext, Boolean.class));
    }

    private boolean unless(EvaluationContext evaluationContext, String unlessExpression) {
        return StrUtil.isBlank(unlessExpression) || Boolean.TRUE.equals(parser.parseExpression(unlessExpression).getValue(evaluationContext, Boolean.class));
    }

    private String key(EvaluationContext evaluationContext, String keyExpression) {
        if (StrUtil.isNotBlank(keyExpression)) {
            return parser.parseExpression(keyExpression).getValue(evaluationContext, String.class);
        }
        return keyExpression;
    }

    private EvaluationContext createEvaluationContext(VarianCacheContext cacheContext, Object result) {
        return evaluationContextFactory.forOperation(cacheContext, result);
    }

    private AbstractVarianOperation parseCacheAnnotation(VarianCache varianCache) {
        return new VarianCacheOperation(varianCache.cacheName(), varianCache.key(), varianCache.condition(), varianCache.unless(), varianCache.timeout(), varianCache.unit());
    }

    private AbstractVarianOperation parseCachePutAnnotation(VarianCachePut varianCachePut) {
        return new VarianCachePutOperation(varianCachePut.cacheName(), varianCachePut.key(), varianCachePut.condition(), varianCachePut.unless(), varianCachePut.timeout(), varianCachePut.unit());
    }

    private AbstractVarianOperation parseCacheEvictAnnotation(VarianCacheEvict varianCacheEvict) {
        return new VarianCacheEvictOperation(varianCacheEvict.cacheName(), varianCacheEvict.key(), varianCacheEvict.condition(), varianCacheEvict.allEntries(), varianCacheEvict.beforeInvocation());
    }

    private MethodClassKey generateMethodClassKey(Class<?> clazz, Method method) {
        return new MethodClassKey(method, clazz);
    }

}
